module.exports = Geometry

var Types = require('./types')
var Point = require('./point')
var LineString = require('./linestring')
var Polygon = require('./polygon')
var MultiPoint = require('./multipoint')
var MultiLineString = require('./multilinestring')
var MultiPolygon = require('./multipolygon')
var GeometryCollection = require('./geometrycollection')
var BinaryReader = require('./binaryreader')
var BinaryWriter = require('./binarywriter')
var WktParser = require('./wktparser')
var ZigZag = require('./zigzag.js')

function Geometry() {
  this.srid = undefined
  this.hasZ = false
  this.hasM = false
}

Geometry.parse = function(value, options) {
  var valueType = typeof value

  if (valueType === 'string' || value instanceof WktParser) { return Geometry._parseWkt(value) } else if (Buffer.isBuffer(value) || value instanceof BinaryReader) { return Geometry._parseWkb(value, options) } else { throw new Error('first argument must be a string or Buffer') }
}

Geometry._parseWkt = function(value) {
  var wktParser,
    srid

  if (value instanceof WktParser) { wktParser = value } else { wktParser = new WktParser(value) }

  var match = wktParser.matchRegex([/^SRID=(\d+);/])
  if (match) { srid = parseInt(match[1], 10) }

  var geometryType = wktParser.matchType()
  var dimension = wktParser.matchDimension()

  var options = {
    srid: srid,
    hasZ: dimension.hasZ,
    hasM: dimension.hasM
  }

  switch (geometryType) {
    case Types.wkt.Point:
      return Point._parseWkt(wktParser, options)
    case Types.wkt.LineString:
      return LineString._parseWkt(wktParser, options)
    case Types.wkt.Polygon:
      return Polygon._parseWkt(wktParser, options)
    case Types.wkt.MultiPoint:
      return MultiPoint._parseWkt(wktParser, options)
    case Types.wkt.MultiLineString:
      return MultiLineString._parseWkt(wktParser, options)
    case Types.wkt.MultiPolygon:
      return MultiPolygon._parseWkt(wktParser, options)
    case Types.wkt.GeometryCollection:
      return GeometryCollection._parseWkt(wktParser, options)
  }
}

Geometry._parseWkb = function(value, parentOptions) {
  var binaryReader
  var wkbType
  var geometryType
  var options = {}

  if (value instanceof BinaryReader) { binaryReader = value } else { binaryReader = new BinaryReader(value) }

  binaryReader.isBigEndian = !binaryReader.readInt8()

  wkbType = binaryReader.readUInt32()

  options.hasSrid = (wkbType & 0x20000000) === 0x20000000
  options.isEwkb = (wkbType & 0x20000000) || (wkbType & 0x40000000) || (wkbType & 0x80000000)

  if (options.hasSrid) { options.srid = binaryReader.readUInt32() }

  options.hasZ = false
  options.hasM = false

  if (!options.isEwkb && (!parentOptions || !parentOptions.isEwkb)) {
    if (wkbType >= 1000 && wkbType < 2000) {
      options.hasZ = true
      geometryType = wkbType - 1000
    } else if (wkbType >= 2000 && wkbType < 3000) {
      options.hasM = true
      geometryType = wkbType - 2000
    } else if (wkbType >= 3000 && wkbType < 4000) {
      options.hasZ = true
      options.hasM = true
      geometryType = wkbType - 3000
    } else {
      geometryType = wkbType
    }
  } else {
    if (wkbType & 0x80000000) { options.hasZ = true }
    if (wkbType & 0x40000000) { options.hasM = true }

    geometryType = wkbType & 0xF
  }

  switch (geometryType) {
    case Types.wkb.Point:
      return Point._parseWkb(binaryReader, options)
    case Types.wkb.LineString:
      return LineString._parseWkb(binaryReader, options)
    case Types.wkb.Polygon:
      return Polygon._parseWkb(binaryReader, options)
    case Types.wkb.MultiPoint:
      return MultiPoint._parseWkb(binaryReader, options)
    case Types.wkb.MultiLineString:
      return MultiLineString._parseWkb(binaryReader, options)
    case Types.wkb.MultiPolygon:
      return MultiPolygon._parseWkb(binaryReader, options)
    case Types.wkb.GeometryCollection:
      return GeometryCollection._parseWkb(binaryReader, options)
    default:
      throw new Error('GeometryType ' + geometryType + ' not supported')
  }
}

Geometry.parseTwkb = function(value) {
  var binaryReader
  var options = {}

  if (value instanceof BinaryReader) { binaryReader = value } else { binaryReader = new BinaryReader(value) }

  var type = binaryReader.readUInt8()
  var metadataHeader = binaryReader.readUInt8()

  var geometryType = type & 0x0F
  options.precision = ZigZag.decode(type >> 4)
  options.precisionFactor = Math.pow(10, options.precision)

  options.hasBoundingBox = metadataHeader >> 0 & 1
  options.hasSizeAttribute = metadataHeader >> 1 & 1
  options.hasIdList = metadataHeader >> 2 & 1
  options.hasExtendedPrecision = metadataHeader >> 3 & 1
  options.isEmpty = metadataHeader >> 4 & 1

  if (options.hasExtendedPrecision) {
    var extendedPrecision = binaryReader.readUInt8()
    options.hasZ = (extendedPrecision & 0x01) === 0x01
    options.hasM = (extendedPrecision & 0x02) === 0x02

    options.zPrecision = ZigZag.decode((extendedPrecision & 0x1C) >> 2)
    options.zPrecisionFactor = Math.pow(10, options.zPrecision)

    options.mPrecision = ZigZag.decode((extendedPrecision & 0xE0) >> 5)
    options.mPrecisionFactor = Math.pow(10, options.mPrecision)
  } else {
    options.hasZ = false
    options.hasM = false
  }

  if (options.hasSizeAttribute) { binaryReader.readVarInt() }
  if (options.hasBoundingBox) {
    var dimensions = 2

    if (options.hasZ) { dimensions++ }
    if (options.hasM) { dimensions++ }

    for (var i = 0; i < dimensions; i++) {
      binaryReader.readVarInt()
      binaryReader.readVarInt()
    }
  }

  switch (geometryType) {
    case Types.wkb.Point:
      return Point._parseTwkb(binaryReader, options)
    case Types.wkb.LineString:
      return LineString._parseTwkb(binaryReader, options)
    case Types.wkb.Polygon:
      return Polygon._parseTwkb(binaryReader, options)
    case Types.wkb.MultiPoint:
      return MultiPoint._parseTwkb(binaryReader, options)
    case Types.wkb.MultiLineString:
      return MultiLineString._parseTwkb(binaryReader, options)
    case Types.wkb.MultiPolygon:
      return MultiPolygon._parseTwkb(binaryReader, options)
    case Types.wkb.GeometryCollection:
      return GeometryCollection._parseTwkb(binaryReader, options)
    default:
      throw new Error('GeometryType ' + geometryType + ' not supported')
  }
}

Geometry.parseGeoJSON = function(value) {
  return Geometry._parseGeoJSON(value)
}

Geometry._parseGeoJSON = function(value, isSubGeometry) {
  var geometry

  switch (value.type) {
    case Types.geoJSON.Point:
      geometry = Point._parseGeoJSON(value); break
    case Types.geoJSON.LineString:
      geometry = LineString._parseGeoJSON(value); break
    case Types.geoJSON.Polygon:
      geometry = Polygon._parseGeoJSON(value); break
    case Types.geoJSON.MultiPoint:
      geometry = MultiPoint._parseGeoJSON(value); break
    case Types.geoJSON.MultiLineString:
      geometry = MultiLineString._parseGeoJSON(value); break
    case Types.geoJSON.MultiPolygon:
      geometry = MultiPolygon._parseGeoJSON(value); break
    case Types.geoJSON.GeometryCollection:
      geometry = GeometryCollection._parseGeoJSON(value); break
    default:
      throw new Error('GeometryType ' + value.type + ' not supported')
  }

  if (value.crs && value.crs.type && value.crs.type === 'name' && value.crs.properties && value.crs.properties.name) {
    var crs = value.crs.properties.name

    if (crs.indexOf('EPSG:') === 0) { geometry.srid = parseInt(crs.substring(5)) } else if (crs.indexOf('urn:ogc:def:crs:EPSG::') === 0) { geometry.srid = parseInt(crs.substring(22)) } else { throw new Error('Unsupported crs: ' + crs) }
  } else if (!isSubGeometry) {
    geometry.srid = 4326
  }

  return geometry
}

Geometry.prototype.toEwkt = function() {
  return 'SRID=' + this.srid + ';' + this.toWkt()
}

Geometry.prototype.toEwkb = function() {
  var ewkb = new BinaryWriter(this._getWkbSize() + 4)
  var wkb = this.toWkb()

  ewkb.writeInt8(1)
  ewkb.writeUInt32LE((wkb.slice(1, 5).readUInt32LE(0) | 0x20000000) >>> 0, true)
  ewkb.writeUInt32LE(this.srid)

  ewkb.writeBuffer(wkb.slice(5))

  return ewkb.buffer
}

Geometry.prototype._getWktType = function(wktType, isEmpty) {
  var wkt = wktType

  if (this.hasZ && this.hasM) { wkt += ' ZM ' } else if (this.hasZ) { wkt += ' Z ' } else if (this.hasM) { wkt += ' M ' }

  if (isEmpty && !this.hasZ && !this.hasM) { wkt += ' ' }

  if (isEmpty) { wkt += 'EMPTY' }

  return wkt
}

Geometry.prototype._getWktCoordinate = function(point) {
  var coordinates = point.x + ' ' + point.y

  if (this.hasZ) { coordinates += ' ' + point.z }
  if (this.hasM) { coordinates += ' ' + point.m }

  return coordinates
}

Geometry.prototype._writeWkbType = function(wkb, geometryType, parentOptions) {
  var dimensionType = 0

  if (typeof this.srid === 'undefined' && (!parentOptions || typeof parentOptions.srid === 'undefined')) {
    if (this.hasZ && this.hasM) { dimensionType += 3000 } else if (this.hasZ) { dimensionType += 1000 } else if (this.hasM) { dimensionType += 2000 }
  } else {
    if (this.hasZ) { dimensionType |= 0x80000000 }
    if (this.hasM) { dimensionType |= 0x40000000 }
  }

  wkb.writeUInt32LE((dimensionType + geometryType) >>> 0, true)
}

Geometry.getTwkbPrecision = function(xyPrecision, zPrecision, mPrecision) {
  return {
    xy: xyPrecision,
    z: zPrecision,
    m: mPrecision,
    xyFactor: Math.pow(10, xyPrecision),
    zFactor: Math.pow(10, zPrecision),
    mFactor: Math.pow(10, mPrecision)
  }
}

Geometry.prototype._writeTwkbHeader = function(twkb, geometryType, precision, isEmpty) {
  var type = (ZigZag.encode(precision.xy) << 4) + geometryType
  var metadataHeader = (this.hasZ || this.hasM) << 3
  metadataHeader += isEmpty << 4

  twkb.writeUInt8(type)
  twkb.writeUInt8(metadataHeader)

  if (this.hasZ || this.hasM) {
    var extendedPrecision = 0
    if (this.hasZ) { extendedPrecision |= 0x1 }
    if (this.hasM) { extendedPrecision |= 0x2 }

    twkb.writeUInt8(extendedPrecision)
  }
}

Geometry.prototype.toGeoJSON = function(options) {
  var geoJSON = {}

  if (this.srid) {
    if (options) {
      if (options.shortCrs) {
        geoJSON.crs = {
          type: 'name',
          properties: {
            name: 'EPSG:' + this.srid
          }
        }
      } else if (options.longCrs) {
        geoJSON.crs = {
          type: 'name',
          properties: {
            name: 'urn:ogc:def:crs:EPSG::' + this.srid
          }
        }
      }
    }
  }

  return geoJSON
}
